require 'rubygems'
require 'active_record'
require 'xmlrpc/client'
require 'redcloth'
require 'time'
require 'base64'
require 'process'

#DataMapper::Database.setup({
#  :adapter  =>  'sqlite3',
#  :database =>  'rcpub.db'
#})

ActiveRecord::Base.establish_connection(
  'adapter' => 'sqlite3',
  'database' => '/Users/nineclue/lab/rcpublisher/rcpub.db'
)

METAWEBLOG = {
  :get_categories=>'metaWeblog.getCategories',
  :new_post => 'metaWeblog.newPost',
  :edit_post => 'metaWeblog.editPost',
  :new_media => 'metaWeblog.newMediaObject'
}

DEFAULT = METAWEBLOG

class Site < ActiveRecord::Base
  has_many :publications
  has_many :articles, :through => :publications

  has_many :medialinks
  has_many :medias, :through => :medialinks

  has_many :categories, :as => :owner, :dependent => :destroy

  # @@fields = []
  
  def get_connection
    # returns xmlrpc client object
    @connection ||= XMLRPC::Client.new2(self.url)
  end

  def get_category_list
    # get array of category names
    categories.collect { |c| c.name }
  end
  
  def refresh_categories
    # get list of categories from server and refreshes database
    new_list = get_connection.call(DEFAULT[:get_categories], self.blogid, self.username, self.passwd)
    if self.respond_to?(:rearange_categories) 
      new_list = self.rearange_categories(new_list)
    end

    current_list = get_category_list
    removed = current_list - new_list
    categories.each { |c| c.destroy if removed.include?(c.name) }
    (new_list - current_list).each { |c| categories << Category.new(:name=>c, :owner=>self) }
  end
  
  def self.define_field(fieldname, type, *default)
    @fields ||= []
    type = type.to_sym
    raise "Unknown format" unless [:text, :bool].include?(type)
    @fields << [fieldname, type, default.nil? ? nil : default[0]]
  end
  
  def self.get_fields
    @fields ||= []
  end
  
  def post_article(article)
    p "Post #{self.name}:"
    struct = before_post(article)  
    if article.has_key?(:pub_id) then
      p "updating #{article[:pubid]}"
      p article
      get_connection.call(DEFAULT[:edit_post], article[:pub_id], username, passwd, struct, true)
    else
      p "creating new one..."
      article[:pub_id] = get_connection.call(DEFAULT[:new_post], blogid, username, passwd, struct, true)
    end
    article
  end
  
  def post_media(media)
    p "Post Media"
    bindata = Base64.decode64(media.b64data)
    attach = {"name"=>media.fname, "type"=>media.ftype, "bits" => XMLRPC::Base64.new(bindata)}
    r = get_connection.call(DEFAULT[:new_media], blogid, username, passwd, attach)    # returns created URI
    get_url(r)
  end
  
  def validate_post(article)
    msgs = []
    msgs << 'Title is empty.' if article[:title].length == 0
    msgs << 'Content is empty.' if article[:content].length == 0
    msgs << 'At least one category should be selected.' if article[:categories].size == 0
    if self.respond_to?(:subvalidate)
      msgs += subvalidate(article)
    end
    msgs.compact!
    (msgs.size == 0) ? true : msgs.join(' ')
  end
  
  def textilize(content)
    RedCloth.new(content).to_html
  end
  
  def before_post(article)
    { "title"=>article[:title], "description"=>article[:content], 
      "categories"=>article[:categories], "mt_keywords"=>article[:tag]
    }
  end
  
  def get_url(r)          # may be different across sites
    r['url']
  end
end

class Media < ActiveRecord::Base
  has_many :medialinks
  has_many :sites, :through => :medialinks
  
  THUMB_WIDTH = 150
  THUMB_HEIGHT = 100
  
  def self.is_picture?(link)
    File.exist?(link) and File.ftype(link) == 'file' and ['jpg', 'jpeg', 'png', 'gif'].include?(File.basename(link.downcase).split('.')[-1])
  end
  
  def self.find_or_create(link)
    return nil unless (File.exist?(link) and File.ftype(link) == 'file')
    fname = File.basename(link)
    fsize = File.size(link)
    media = find(:first, :conditions=>{:fname=>fname, :fsize=>fsize})
    return media unless media.nil?
    b64 = open(link) { |f| Base64.encode64(f.read) }
    ftype = guess_mime_type(fname)
    if ftype.split('/')[0] == 'image' then
      p = Processor.new(OSX::CIImage.from(link))
      p.fit(THUMB_WIDTH)
      tb64 = p.render { |r| Base64.encode64(r.rubyString) }
    else
      tb64 = Base64.encode64(filename_thumb(fname))
    end
    p "creating new media #{fname}..."
    create(:fname=>fname, :fsize=>fsize, :ftype=>ftype, :b64data=>b64, :b64thumb=>tb64)
  end
  
  def self.filename_thumb(fname)
    canvas = OSX::NSBitmapImageRep.alloc.initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(nil, THUMB_WIDTH, THUMB_HEIGHT, 8, 4, true, false, OSX::NSDeviceRGBColorSpace, 0, 0)
    canvas_context = OSX::NSGraphicsContext.graphicsContextWithBitmapImageRep(canvas)
    OSX::NSGraphicsContext.setCurrentContext(canvas_context)
    
    names = fname.split('.')
    name = OSX::NSString.alloc.initWithString(names[0])
    font = OSX::NSFont.fontWithName_size('Helvetica', 16)
    d = OSX::NSDictionary.dictionaryWithObject_forKey(font, OSX::NSFontAttributeName)
    name_x = (THUMB_WIDTH - name.sizeWithAttributes(d).width) / 2
    name_y = THUMB_HEIGHT / 2 + 3
    name.drawAtPoint_withAttributes([name_x, name_y], d)

    unless names[1].nil? then
      ext = OSX::NSString.alloc.initWithString(names[1])
      ext_x = (THUMB_WIDTH - ext.sizeWithAttributes(d).width) / 2
      ext_y = THUMB_HEIGHT / 2 - ext.sizeWithAttributes(d).height - 3
      ext.drawAtPoint_withAttributes([ext_x, ext_y], d)
    end
    
    canvas.representationUsingType_properties(OSX::NSJPEGFileType, nil).rubyString
  end
  
  def self.guess_mime_type(fname)
    case fname.downcase.split('.')[-1]
    when 'jpg' || 'jpeg'
      'image/jpeg'
    when 'png'
      'image/x-png'
    when 'gif'
      'image/gif'
    when 'bmp'
      'image/bmp'
    when 'pdf'
      'application/pdf'
    when 'zip'
      'application/zip-compressed'
    when 'gz' || 'tgz'
      'application/x-gzip-compressed'
    when 'exe'
      'application/x-msdownload'
    else
      'application/octet-stream'
    end
  end
end

class Medialink < ActiveRecord::Base
  belongs_to :site
  belongs_to :media
end

class Article < ActiveRecord::Base
  has_many :publications
  has_many :sites, :through => :publications
  has_many :categories, :as => :owner, :dependent => :destroy
  serialize :custom
  
  def self.validate_save(title, content)
    title.strip.length > 0 ? nil : 'Title can\'t be blank...'
  end
  
  def self.get_recent_articles(limit=10)
    find(:all, :order=>'updated_at DESC', :limit=>limit, 
          :select=>'DISTINCT articles.id, title',
          :joins=>'INNER JOIN publications ON articles.id = publications.article_id',
          :conditions=>'published_at IS NOT NULL')
  end
  
  def self.get_drafts(limit=0)
    param = { :order=>'updated_at DESC',
              :select=>'DISTINCT articles.id, title',
              :joins=>'LEFT JOIN publications ON articles.id = publications.article_id',
              :conditions=>'published_at IS NULL' }
    param[:limit] = limit if limit > 0
    find(:all, param)
  end
  
  def self.get_start_year
    start_article = find(:first, :order=>'published_at', 
                         :joins=>'INNER JOIN publications ON article_id',
                         :conditions=>'published_at IS NOT NULL')
    (start_article.nil?) ? nil : start_article.published_at.year
  end
  
  def self.get_monthly_articles
    find(:all, :order=>'published_at', :select=>'articles.id, title, published_at',
         :joins=>'INNER JOIN publications ON article_id',
         :conditions=>'published_at IS NOT NULL',
         :group=>"date(published_at, 'start of month')")
  end

  def get_category_list
    # get array of category names
    categories.collect { |c| c.name }
  end  
end

class Publication < ActiveRecord::Base
  belongs_to :article
  belongs_to :site
end

class Category < ActiveRecord::Base
  belongs_to :owner, :polymorphic => true
  
  # cf) http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#M001103, Polymorphic association with STI
  def owner_type=(sType)
    super(sType.to_s.classify.constantize.base_class.to_s)
  end
end

class Mephisto < Site
  define_field :link, :text
  define_field :comment_age, :text, 30
  
  def subvalidate(article)
    p article
    msgs = []
    link = article[:class][:Mephisto][:link]
    msgs << "Non-acceptable characters in link #{article[:link]}" if link.size > 0 and link.match('(([\.\'\"/%:&?]|\w)*)')[0].size == 0
    msgs
  end
  
  def before_post(article)
    link = article[:class][:Mephisto][:link]
    comment_age = article[:class][:Mephisto][:comment_age]
    article = super(article)
    article['permaLink'] = link
    article['comment_age'] = comment_age
    article
  end
end

class Tistory < Site
  IGNORE_CATEGORY = ["Random"]
  
  # return a list of category names
  def rearange_categories(categories)
    categories.collect { |c| c['categoryName'] }
  end

  def before_post(article)
    article = super(article)
    article['categories'] -= IGNORE_CATEGORY if article['categories'].size > 1
    article['description'] = textilize(article['description'])
    article
  end
end
